use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::res::MaybeDef;
use clippy_utils::source::{snippet, snippet_with_applicability};
use clippy_utils::sugg::Sugg;
use clippy_utils::{get_parent_expr, sym};
use rustc_ast::LitKind;
use rustc_errors::Applicability;
use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
use rustc_hir::{BinOpKind, Closure, Expr, ExprKind, QPath};
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::{Span, Symbol};

use super::MANUAL_IS_VARIANT_AND;

pub(super) fn check(
    cx: &LateContext<'_>,
    expr: &Expr<'_>,
    map_recv: &Expr<'_>,
    map_arg: &Expr<'_>,
    map_span: Span,
    msrv: Msrv,
) {
    // Don't lint if:

    // 1. the `expr` is generated by a macro
    if expr.span.from_expansion() {
        return;
    }

    // 2. the caller of `map()` is neither `Option` nor `Result`
    let is_option = cx.typeck_results().expr_ty(map_recv).is_diag_item(cx, sym::Option);
    let is_result = cx.typeck_results().expr_ty(map_recv).is_diag_item(cx, sym::Result);
    if !is_option && !is_result {
        return;
    }

    // 3. the caller of `unwrap_or_default` is neither `Option<bool>` nor `Result<bool, _>`
    if !cx.typeck_results().expr_ty(expr).is_bool() {
        return;
    }

    // 4. msrv doesn't meet `OPTION_RESULT_IS_VARIANT_AND`
    if !msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
        return;
    }

    let lint_msg = if is_option {
        "called `map(<f>).unwrap_or_default()` on an `Option` value"
    } else {
        "called `map(<f>).unwrap_or_default()` on a `Result` value"
    };
    let suggestion = if is_option { "is_some_and" } else { "is_ok_and" };

    span_lint_and_sugg(
        cx,
        MANUAL_IS_VARIANT_AND,
        expr.span.with_lo(map_span.lo()),
        lint_msg,
        "use",
        format!("{}({})", suggestion, snippet(cx, map_arg.span, "..")),
        Applicability::MachineApplicable,
    );
}

#[derive(Clone, Copy, PartialEq)]
enum Flavor {
    Option,
    Result,
}

impl Flavor {
    const fn symbol(self) -> Symbol {
        match self {
            Self::Option => sym::Option,
            Self::Result => sym::Result,
        }
    }

    const fn positive(self) -> Symbol {
        match self {
            Self::Option => sym::Some,
            Self::Result => sym::Ok,
        }
    }
}

#[derive(Clone, Copy, PartialEq)]
enum Op {
    Eq,
    Ne,
}

impl std::fmt::Display for Op {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Eq => write!(f, "=="),
            Self::Ne => write!(f, "!="),
        }
    }
}

impl TryFrom<BinOpKind> for Op {
    type Error = ();
    fn try_from(op: BinOpKind) -> Result<Self, Self::Error> {
        match op {
            BinOpKind::Eq => Ok(Self::Eq),
            BinOpKind::Ne => Ok(Self::Ne),
            _ => Err(()),
        }
    }
}

/// Represents the argument of the `.map()` function, as a closure or as a path
/// in case η-reduction is used.
enum MapFunc<'hir> {
    Closure(&'hir Closure<'hir>),
    Path(&'hir Expr<'hir>),
}

impl<'hir> TryFrom<&'hir Expr<'hir>> for MapFunc<'hir> {
    type Error = ();

    fn try_from(expr: &'hir Expr<'hir>) -> Result<Self, Self::Error> {
        match expr.kind {
            ExprKind::Closure(closure) => Ok(Self::Closure(closure)),
            ExprKind::Path(_) => Ok(Self::Path(expr)),
            _ => Err(()),
        }
    }
}

impl<'hir> MapFunc<'hir> {
    /// Build a suggestion suitable for use in a `.map()`-like function. η-expansion will be applied
    /// as needed.
    fn sugg(self, cx: &LateContext<'hir>, invert: bool, app: &mut Applicability) -> String {
        match self {
            Self::Closure(closure) => {
                let body = Sugg::hir_with_applicability(cx, cx.tcx.hir_body(closure.body).value, "..", app);
                format!(
                    "{} {}",
                    snippet_with_applicability(cx, closure.fn_decl_span, "|..|", app),
                    if invert { !body } else { body }
                )
            },
            Self::Path(expr) => {
                let path = snippet_with_applicability(cx, expr.span, "_", app);
                if invert {
                    format!("|x| !{path}(x)")
                } else {
                    path.to_string()
                }
            },
        }
    }
}

fn emit_lint<'tcx>(
    cx: &LateContext<'tcx>,
    span: Span,
    op: Op,
    flavor: Flavor,
    in_some_or_ok: bool,
    map_func: MapFunc<'tcx>,
    recv: &Expr<'_>,
) {
    let mut app = Applicability::MachineApplicable;
    let recv = snippet_with_applicability(cx, recv.span, "_", &mut app);

    let (invert_expr, method, invert_body) = match (flavor, op) {
        (Flavor::Option, Op::Eq) => (false, "is_some_and", !in_some_or_ok),
        (Flavor::Option, Op::Ne) => (false, "is_none_or", in_some_or_ok),
        (Flavor::Result, Op::Eq) => (false, "is_ok_and", !in_some_or_ok),
        (Flavor::Result, Op::Ne) => (true, "is_ok_and", !in_some_or_ok),
    };
    span_lint_and_sugg(
        cx,
        MANUAL_IS_VARIANT_AND,
        span,
        format!("called `.map() {op} {pos}()`", pos = flavor.positive(),),
        "use",
        format!(
            "{inversion}{recv}.{method}({body})",
            inversion = if invert_expr { "!" } else { "" },
            body = map_func.sugg(cx, invert_body, &mut app),
        ),
        app,
    );
}

pub(super) fn check_map(cx: &LateContext<'_>, expr: &Expr<'_>) {
    if let Some(parent_expr) = get_parent_expr(cx, expr)
        && let ExprKind::Binary(op, left, right) = parent_expr.kind
        && op.span.eq_ctxt(expr.span)
        && let Ok(op) = Op::try_from(op.node)
    {
        // Check `left` and `right` expression in any order, and for `Option` and `Result`
        for (expr1, expr2) in [(left, right), (right, left)] {
            for flavor in [Flavor::Option, Flavor::Result] {
                if let ExprKind::Call(call, [arg]) = expr1.kind
                    && let ExprKind::Lit(lit) = arg.kind
                    && let LitKind::Bool(bool_cst) = lit.node
                    && let ExprKind::Path(QPath::Resolved(_, path)) = call.kind
                    && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Fn), _) = path.res
                    && let ty = cx.typeck_results().expr_ty(expr1)
                    && let ty::Adt(adt, args) = ty.kind()
                    && cx.tcx.is_diagnostic_item(flavor.symbol(), adt.did())
                    && args.type_at(0).is_bool()
                    && let ExprKind::MethodCall(_, recv, [map_expr], _) = expr2.kind
                    && cx.typeck_results().expr_ty(recv).is_diag_item(cx, flavor.symbol())
                    && let Ok(map_func) = MapFunc::try_from(map_expr)
                {
                    return emit_lint(cx, parent_expr.span, op, flavor, bool_cst, map_func, recv);
                }
            }
        }
    }
}
